C++의 클래스 정의(class definition)는 클래스 인터페이스만 지정하는 것이 아닌,
구현 세부사항까지 많은 부분을 지정하고 있다.
class Person{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthData() const;
std::string address() const;
private:
std::string theName;
Date theBirthDate;
Address theAddress;
};
위의 코드를 컴파일하는데 추가적으로 string, Date, Address와 같은 자료형의 정의를 필요로 한다.
#include 지시자를 이용해서 위의 자료형의 정의된 정보를 가져와서 사용한다.
#include <string>
#include "date.h"
#include "address.h"
하지만 위의 #inlcude문은Person을 정의한 파일과 위의 헤더 파일들 사이의 컴파일 의존성(compile dependency)를 야기한다.
위의 헤더 파일 셋 중에 하나라도 수정되는 경우,
Person 클래스를 정의한 파일은 모두 다시 컴파일 처리를 해 주어야 한다.
namespace std{
class string;
}
class Date;
class Address;
class Person{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
};
만일 위처럼 선언할 경우, Person 클래스의 인터페이스가 바뀌었을 때만, 컴파일을 다시 해주면 된다.
하지만, 위는 잘못된 표현이다.
실제로는 string은 typedef로 통해 정의된 basic_string<char>의 타입동의어이다.(class가 아님)
표준 라이브러리 구성 요소는 전방 선언하지 않음
또한, 컴파일러는 필요한 요소들을 전방 선언할 때, 객체들의 크기를 모두 알고 있어야 한다.
int main(void){
int x;
Person p(params);
}
위 코드가 컴파일 될 때, 컴파일러는 미리 int(x)와 Person(p)의 크기를 알아야 한다.
Java와 같은 언어는 내부적으로 모든 객체를 포인터를 담을 공간에 할당하기 때문에 크기를 컴파일 시간에 직접
알고자 하지는 않는다.
int main(void){
int x;
Person* p;
}
C++에서 위와 같이 포인터 뒤에 실제 객체 구현부를 숨기는 방법도 사용 가능하다.
#include <string>
#include <memory>
class PersonImpl;
class Date;
class Address;
class Person{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::shared_ptr<PersonImpl> pImpl;
};
위와 같이 구현하면, Person class를 생일, 주소, 이름 등의 클래스와 컴파일 타임에 구분할 수 있다.
사용자 입장에서는 Person에 대한 구현 세부사항을 수정하여도 컴파일을 다시 할 필요가 없다.
pImple 관용구를 사용하는 Person 클래스를 핸들 클래스(handle class)라고 부른다.
인터페이스와 구현을 나누기 위해서는,
정의부에 대한 의존성(dependencies on definition)을
선언부에 대한 의존성(dependencies on definition)으로 바꾸는데 있다.
- 객체 참조자 및 포인터로 충분한 경우, 객체를 직접 사용하지 않는다.참조자 및 포인터만 정의할 때는 해당 타입의 선언부만 필요로 한다.(선언부 의존성)
반면 타입에 대한 객체를 정의할 때는 타입의 정의를 필요로 한다.(정의부 의존성)
- 가능하다면, 클래스 정의 대신 클래스 선언에 최대한 의존하도록 구현
클래스에서 사용하는 함수를 선언할 때, 해당 클래스의 정의를 가져오지 않아도 괜찮음
class Date;
Date today();
void clearAppointments(Date d);
- 선언부와 정의부에 대한 별도의 헤더 파일을 제공사용자 코드에서는 전방선언 대신, 선언부 헤더 파일을 #inlcude 하여 사용할 수 있다.
라이브러리 제작자는 헤더 파일을 두 개의 쌍(선언부 헤더, 정의부 헤더)으로 제공해야 한다.
#include "datefwd.h"
Date today();
void clearAppointments(Date d);
C++ 은 iostream 관련 함수 및 클래스들의 선언부만으로 구성된 <iosfwd> 헤더를 제공한다.
<sstream>, <streambuf>, <fstream>, <iostream> 등에 나위어서 정의되어 있음
C++은 템플릿 선언과 템플릿 정의를 분리할 수 있는 export 키워드를 제공한다.
하지만, 대부분의 컴파일러가 아직 export 키워드를 제대로 지원하지 않는다.
(아직까지 대부분 헤더파일에 템플릿 정의를 포함시켜서 사용함)
핸들 클래스 사용법
#include "Person.h"
#include "PersonImpl.h"
Person::Person(const std::string& name, const Date& birthday, const Address& addr): pImpl(new PersonImpl(name, birth, addr)) {}
std::string Person::name() const{
return pImpl->name();
}
혹은 Person 클래스를 추상 기본 클래스, 인터페이스 클래스(Interface class)로 만들어 사용할 수 있다.
class Person{
public:
virtual ~Person();
virtual std::string name() const =0;
virtual std::string name() const =0;
virtual std::string address() const =0;
};
순수 가상 함수를 포함한 클래스는 인스턴스를 만드는 것이 불가능하기 때문에,
이를 이용하기 위해서는 Person에 대한 푄터 혹은 참조자로 프로그래밍 해야 한다.
인터페이스 클래스의 인터페이스가 수정되지 않는한, 사용자가 이를 다시 컴파일할 일은 없다.
팩토리함수(가상 생성자)를 이용해서 위의 인터페이스를 사용자는 이용한다.
class Person{
public:
static std::shared_ptr<Person> create(const std::string& name, const Date& birthday, const address& addr);
};
std::string name;
Date dateofBirth;
Address address;
std::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
std::cout<<pp->name()<<" was born on "<<pp->birthDate()<<" and now lives at "<<pp->address();
class RealPerson: public Person{
public:
RealPerson(const std::string& name, const Date& birthday, const Address& addr): theName(name), theBirthDate(birthday), theAddress(addr) {}
virtual ~RealPerson() {}
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::string theName;
Date theBirthDate;
Address theAddress;
};
std::shared_ptr<Person> Person::create(const std::string& name, const Date& birthday, const Address& addr){
return std::shared_ptr<Person>(new RealPerson(name, birthday, addr));
}
인터페이스 클래스를 구현할 때, 위와 같이 인터페이스 클래스를 상속한 파생클래스에서 가상함수를 구현하거나
다중 상속을 이용할 수 있다.
인라인 함수와 템플릿(export를 이용해서 나눈는 것이 가능하기는 함) 대게 헤더 파일에 정의를 둔다.
따라서 핸들 클래스와 인터페이스는 인라인 함수와 상반되기 때문에 잘 맞지 않다.